/****************************************************************
 ** Product  :   HP Subscriber Network Application Policy
 ** Module   :   gr-manager
 ** Date: Apr 10, 2015               
 ** Author: Joey Yi
 ** (C) Copyright 2015, Hewlett-Packard Company, All Rights Reserved.
 ** This software is the proprietary information of HP, Inc.  
 ** Use is subject to license terms.
 *****************************************************************
 ** Description:   
 **      Execute action defined in a action plan. 
 **    
 ****************************************************************/
package com.hp.snap.gr.action.impl;


import com.hp.snap.gr.GRMLauncher;
import com.hp.snap.gr.action.ActionHandler;
import com.hp.snap.gr.action.ActionPlanService;
import com.hp.snap.gr.exception.ActionException;
import com.hp.snap.gr.model.jaxb.*;
import com.hp.snap.gr.utils.*;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

/**
 * Execute action defined in a action plan.
 * A <PreCondition> node can be set in the <Action> node, need to check if match the condition which defined in <PreCondition> node,
 * if match, the action will be executed; if not match, the action will be skipped. 
 * There is no <PreCondition> node setting, the action will be must executed.
 * 
 *  A action execution timeout, a error log message will be sent trap. The timeout parameter configure in the grm.properties.
 *
 * <P>
 * Version: 3.3 <br>
 * Author: Joey Yi
 * Date: Apr 10, 2015
 * 
 * </P>
 *
 **/
public class ActionPlanServiceImpl implements ActionPlanService {

    public static Map<String, String> config = new HashMap<String, String>();
    private static Logger logger = LoggerFactory.getLogger(ActionPlanServiceImpl.class);

    @Override
    public void setGlobalValue(Map<String, String> config) {
        this.config = config;
    }

    @Override
    public Map<String, Object> execute(ActionPlanType actionPlan, boolean isAsync) throws ActionException {
        if (actionPlan == null) {
            throw new ActionException("The action plan cannot be null!");
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Starting to execute action plan: {}", actionPlan.getName());
        }

        // evaluate the action plan input parameters
        Map<String, Object> actionPlanInputs = new HashMap<String, Object>();

        InputOutParamType input = actionPlan.getInput();
        if (input != null) {
            Map<String, Object> context = GRMConfigManager.getActionPlanInputContext();
            List<ParameterType> inputParams = input.getParameter();
            for (ParameterType inputParam : inputParams) {
                String originalValue = !StringUtils.isEmpty(inputParam.getVal()) ? inputParam.getVal() : inputParam.getValue();
                String value = ExpressionHelper.executeTemplate(context, originalValue);
                actionPlanInputs.put(inputParam.getName(), value);
            }
        }

        // create a local timer
        final Timer timer = new Timer("ActionTimer-" + actionPlan.getName());

        // construct the action plan output parameters
        Map<String, Object> actionPlanOutputs = new HashMap<String, Object>();
        actionPlanOutputs.put(Constants.GET_SESSIONSERVER_CONFIG, config);
        // construct the expression and template context for action and action output
        Map<String, Object> context = GRMConfigManager.getContext(actionPlanInputs, actionPlanOutputs);

        // execute the actions in plan
        if (actionPlan.getActionList() != null && !actionPlan.getActionList().getAction().isEmpty()) {
            for (ActionRefType actionRef : actionPlan.getActionList().getAction()) {
                ActionType actionType = GRMTaskHelper.getActionMap().get(actionRef.getName());

                // check the precondition defined in action reference
                if (!checkActionPrecondition(context, actionRef)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Precondition not matched, skip action: {}", actionRef.getName());
                    }
                    continue;
                }

                if (logger.isDebugEnabled()) {
                    logger.debug("Executing action: name={}, type={}", actionRef.getName(), actionType.getClass().getSimpleName());
                }

                // manual intervention, it will be disabled in async mode
                if (!isAsync && actionType.isManualIntervention()) {
                    manualIntervention(actionType);
                }

                TimeoutTask timeoutTask = null;
                try {
                    // get action handler instance
                    ActionHandler actionHandler = ActionHandlerFactory.getActionHandler(actionType);

                    // clone the action definition for pre-process
                    ActionType clonedActionType = (ActionType) actionType.clone();

                    // pre-process the action definition to make the template value
                    actionHandler.preProcess(clonedActionType, context);

                    // register a timeout timer
                    long actionTimeoutConfig = clonedActionType.getTimeout();
                    if (actionTimeoutConfig <= 0) {
                        // default value is 11 minutes
                        String defaultActionTimeout = GRMConfigManager.getLocalConfigValue(Constants.CFG_GRL_ACTION_TIMEOUT, Constants.DEFAULT_ACTION_TIMEOUT);
                        actionTimeoutConfig = Long.parseLong(defaultActionTimeout);
                    }

                    // get the delay time in milliseconds
                    long delay = actionTimeoutConfig * 1000;
                    timeoutTask = new TimeoutTask(actionPlan.getName(), actionRef.getName(), delay);
                    timer.schedule(timeoutTask, delay);

                    if (logger.isDebugEnabled()) {
                        logger.debug("The action timeout period is: {} ms", delay);
                    }
                    
                    // execute the action                    
                    actionHandler.execute(clonedActionType, actionPlanInputs, actionPlanOutputs);
                    
                } catch (Exception e) {
                    if (!isIgnoreException(actionType, context)) {
                        logger.error("Action execute failed: " + actionRef, e);
                        if (e instanceof ActionException) {
                            throw (ActionException) e;
                        } else {
                            throw new ActionException(e);
                        }
                    } else {
                        logger.info("Action execute failed: {}", actionRef.getName());
                        if (logger.isDebugEnabled()) {
                            logger.debug(e.getMessage(), e);
                        }
                    }
                } finally {
                    if (timeoutTask != null) {
                        timeoutTask.cancel();
                        timer.purge();
                    }
                }
            }

        } else {
            logger.info("No actions to execute in plan: {}", actionPlan.getName());
        }

        // only return the defined output parameters
        Map<String, Object> returnActionPlanOutputs = constructActionPlanOutputs(actionPlan, actionPlanOutputs, context);

        if (logger.isTraceEnabled()) {
            logger.trace("Action Plan output parameters: {}", returnActionPlanOutputs);
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Finished to execute action plan: {}", actionPlan.getName());
        }

        return returnActionPlanOutputs;
    }

    private void manualIntervention(ActionType actionType) {
        String hint = actionType.getManualInterventionHint();

        if (StringUtils.isEmpty(hint)) {
            hint = "Are you sure to execute action: [" + actionType.getName() + "]?";
        }

        String confirmTip = hint + " (Yes/No):\n";
        System.out.print(confirmTip);
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        try {
            String confirm = br.readLine();
            while (!"yes".equalsIgnoreCase(confirm) && !"no".equalsIgnoreCase(confirm)) {
                System.out.print(confirmTip);
                confirm = br.readLine();
            }

            if ("no".equalsIgnoreCase(confirm)) {
                logger.info("Action cancelled: {}", actionType.getName());
                GRMLauncher.exitSystem(GRMLauncher.RESULT_SUCCESS);
            }
        } catch (IOException e) {
            logger.info("Failed to confirm the action execution", e);
            GRMLauncher.exitSystem(GRMLauncher.RESULT_SUCCESS);
        }
    }

    private boolean checkActionPrecondition(Map<String, Object> context, ActionRefType actionRef) throws ActionException {
        String precondition = actionRef.getPrecondition();
        if (StringUtils.isNotEmpty(precondition)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Precondition is set, start to verify the precondition... {}", precondition);
            }

            Object obj = ExpressionHelper.executeExpression(context, precondition);
            if (!(obj instanceof Boolean)) {
                throw new ActionException("Action precondition configuration error: " + actionRef.getName());
            }

            return Boolean.valueOf(String.valueOf(obj));

        }

        return true;
    }

    private Map<String, Object> constructActionPlanOutputs(ActionPlanType actionPlan, Map<String, Object> actionPlanOutputs,
                                                           Map<String, Object> context) throws ActionException {
        Map<String, Object> returnActionPlanOutputs = new HashMap<String, Object>();

        InputOutParamType output = actionPlan.getOutput();
        if (output != null) {
            List<ParameterType> parameterTypes = output.getParameter();
            for (ParameterType parameterType : parameterTypes) {
                if (actionPlanOutputs.containsKey(parameterType.getName())) {
                    returnActionPlanOutputs.put(parameterType.getName(), actionPlanOutputs.get(parameterType.getName()));
                } else {
                    String originalValue = !StringUtils.isEmpty(parameterType.getVal()) ? parameterType.getVal() : parameterType.getValue();
                    String value = ExpressionHelper.executeTemplate(context, originalValue);
                    returnActionPlanOutputs.put(parameterType.getName(), value);
                }
            }
        }
        return returnActionPlanOutputs;
    }

    private boolean isIgnoreException(ActionType action, Map<String, Object> context) {
        String allowFailCondition = action.getAllowFailCondition();

        if (StringUtils.isEmpty(allowFailCondition)) {
            return false;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Allow fail condition is set, start to verify the condition... {}", allowFailCondition);
        }

        Object obj;
        try {
            obj = ExpressionHelper.executeExpression(context, allowFailCondition);
        } catch (ActionException e) {
            logger.error("Failed to execute the AllowFailCondition", e);
            return false;
        }

        if (!(obj instanceof Boolean)) {
            logger.error("Allow fail condition configuration error: " + action.getName());
            return false;
        }

        if (Boolean.valueOf(String.valueOf(obj))) {
            if (logger.isInfoEnabled()) {
                logger.info("Action allow fail, ignore the action exception: ActionName={}", action.getName());
            }
            return true;
        } else {
            return false;
        }
    }

    public class TimeoutTask extends TimerTask {

        private String actionPlanName;
        private String actionName;
        private long delay;

        public TimeoutTask(String actionPlanName, String actionName, long delay) {
            this.actionPlanName = actionPlanName;
            this.actionName = actionName;
            this.delay = delay;
        }

        public String getActionPlanName() {
            return actionPlanName;
        }

        public String getActionName() {
            return actionName;
        }

        public long getDelay() {
            return delay;
        }

        @Override
        public void run() {
            String messageText = "Action [" + getActionPlanName() + "-" + getActionName() + "] execution timeout after [" + getDelay() + "] ms!";
            logger.error(messageText);
            try {
                SNMPTrapHelper.sendTrap(messageText);
            } catch (ActionException e) {
                logger.error(e.getMessage(), e);
            }

            GRMLauncher.exitSystem(GRMLauncher.RESULT_TASK_FAIL);
        }
    }
    
    public class ActionExecutionTask implements Runnable{
        
        private ActionHandler actionHandler;
        private ActionType clonedActionType;
        private Map<String, Object> actionPlanInputs;
        private Map<String, Object> actionPlanOutputs;
        
         public ActionExecutionTask(ActionHandler actionHandler, ActionType clonedActionType, Map<String, Object> actionPlanInputs, Map<String, Object> actionPlanOutputs){
             this.actionHandler = actionHandler;
             this.clonedActionType = clonedActionType;
             this.actionPlanInputs = actionPlanInputs;
             this.actionPlanOutputs = actionPlanOutputs;
         }
         
         @Override
         public void run() {
             try {
                 actionHandler.execute(clonedActionType, actionPlanInputs, actionPlanOutputs);
             }catch(ActionException e){
                 logger.error(e.getMessage(), e);
             }
         }
    }
}
